﻿using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace ProLinq.Toolkit
{
	public static class TypeExtensions
	{
		/// <summary>
		/// Checks if T is assignable from the type.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="type"></param>
		/// <returns></returns>
		public static Boolean Is<T>(this Type type)
		{
			return typeof (T).IsAssignableFrom(type);
		}

		/// <summary>
		/// Checks if the type is derived from specified type.
		/// </summary>
		/// <param name="type1"></param>
		/// <param name="type2"></param>
		/// <returns></returns>
		public static Boolean Is(this Type type1, Type type2)
		{
			if (type1 == type2)
				return true;

			if (type2.IsAssignableFrom(type1))
			{
				return true;
			}

			if (type2.IsGenericTypeDefinition && type1.IsGenericType)
			{
				var gtype1 = type1.IsGenericTypeDefinition ? type1 : type1.GetGenericTypeDefinition();

				if (gtype1 == type2)
					return true;

				if (type2.IsInterface)
				{
					if (gtype1.GetInterfaces().Where(itype => itype.IsGenericType)
						.Select(itype => itype.GetGenericTypeDefinition()).Any(itype => itype == type2))
					{
						return true;
					}
				}

				if (gtype1.BaseType != null)
				{
					return gtype1.BaseType.Is(type2);
				}
			}
			return false;
		}

		public static Boolean IsAnonymousType(this Type type)
		{
			Boolean hasCompilerGeneratedAttribute = type.GetCustomAttributes(typeof (CompilerGeneratedAttribute), false).Length >
			                                        0;
			Boolean nameContainsAnonymousType = type.FullName.Contains("AnonymousType");
			Boolean isAnonymousType = hasCompilerGeneratedAttribute && nameContainsAnonymousType;

			return isAnonymousType;
		}

		/// <summary>
		/// Returns true if type is enumerable and not string.
		/// </summary>
		/// <param name="type"></param>
		/// <returns></returns>
		public static Boolean IsEnumerable(this Type type)
		{
			return typeof (IEnumerable).IsAssignableFrom(type) && type != typeof (String);
		}

		/// <summary>
		/// Returns T from first IEnumerable&lt;T&gt; or it's derived type.
		/// Returns Object for non generic IEnumerable.
		/// </summary>
		/// <param name="seqType"></param>
		/// <returns></returns>
		public static Type GetEnumerableType(this Type seqType)
		{
			if (!seqType.IsEnumerable())
			{
				throw new ArgumentException("The type is not enumerable.");
			}

			return FindEnumerableType(seqType);
		}

		private static Type FindEnumerableType(Type seqType)
		{
			if (seqType.IsArray)
				return seqType.GetElementType();

			if (seqType.IsInterface)
			{
				if (seqType == typeof (IEnumerable))
				{
					return typeof (Object);
				}
				if (seqType.IsGenericType && seqType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
				{
					return seqType.GenericTypeArguments[0];
				}
			}

			var ienum = seqType.GetInterfaces().FirstOrDefault(iface => iface.Is<IEnumerable>());
			if (ienum != null)
			{
				return FindEnumerableType(ienum);
			}

			return null;
		}
	}
}